"""
TypeScript symbol walker for traversing AST and extracting symbols.

Provides advanced AST traversal and symbol extraction for TypeScript/JavaScript code,
including scope analysis, symbol resolution, and semantic information.
"""

from typing import Dict, List, Any, Optional, Set, Union
from pathlib import Path
import re

from ...core.error_handler import AnalysisError, log_debug


class SymbolWalker:
    """Walker for TypeScript/JavaScript AST symbols and scopes."""
    
    def __init__(self):
        """Initialize the symbol walker."""
        self.symbols = {}
        self.scopes = []
        self.current_scope = None
        self.symbol_id_counter = 0
        
    def walk_parsed_file(self, parsed_data: Dict[str, Any]) -> Dict[str, Any]:
        """Walk a parsed TypeScript file and extract detailed symbol information."""
        if not parsed_data.get('success'):
            raise AnalysisError(f"Cannot walk failed parse: {parsed_data.get('error', 'Unknown error')}")
        
        file_path = parsed_data.get('fileName', 'unknown')
        
        # Initialize walker state
        self.symbols = {}
        self.scopes = []
        self.current_scope = None
        self.symbol_id_counter = 0
        
        # Create file-level scope
        file_scope = self._create_scope('file', file_path, 1, None)
        self._enter_scope(file_scope)
        
        # Process different types of declarations
        self._process_functions(parsed_data.get('functions', []))
        self._process_classes(parsed_data.get('classes', []))
        self._process_interfaces(parsed_data.get('interfaces', []))
        self._process_types(parsed_data.get('types', []))
        self._process_variables(parsed_data.get('variables', []))
        self._process_imports(parsed_data.get('imports', []))
        self._process_exports(parsed_data.get('exports', []))
        
        # Build call relationships
        call_relationships = self._build_call_relationships(
            parsed_data.get('callExpressions', []),
            parsed_data.get('propertyAccess', [])
        )
        
        # Exit file scope
        self._exit_scope()
        
        return {
            'file_path': file_path,
            'symbols': self.symbols,
            'scopes': self.scopes,
            'call_relationships': call_relationships,
            'symbol_count': len(self.symbols),
            'scope_count': len(self.scopes)
        }
    
    def _create_scope(self, scope_type: str, name: str, line: int, parent_scope: Optional[str]) -> Dict[str, Any]:
        """Create a new scope."""
        scope_id = f"scope_{len(self.scopes)}"
        scope = {
            'id': scope_id,
            'type': scope_type,
            'name': name,
            'line': line,
            'parent': parent_scope,
            'children': [],
            'symbols': [],
            'variables': {},
            'functions': {},
            'classes': {},
            'types': {}
        }
        
        self.scopes.append(scope)
        
        # Add to parent scope children
        if parent_scope:
            for s in self.scopes:
                if s['id'] == parent_scope:
                    s['children'].append(scope_id)
                    break
        
        return scope
    
    def _enter_scope(self, scope: Dict[str, Any]):
        """Enter a scope."""
        self.current_scope = scope['id']
    
    def _exit_scope(self):
        """Exit current scope."""
        if self.current_scope:
            current = self._get_scope(self.current_scope)
            self.current_scope = current['parent'] if current else None
    
    def _get_scope(self, scope_id: str) -> Optional[Dict[str, Any]]:
        """Get scope by ID."""
        for scope in self.scopes:
            if scope['id'] == scope_id:
                return scope
        return None
    
    def _create_symbol(self, symbol_type: str, name: str, line: int, 
                      additional_info: Dict[str, Any] = None) -> str:
        """Create a new symbol."""
        symbol_id = f"symbol_{self.symbol_id_counter}"
        self.symbol_id_counter += 1
        
        symbol = {
            'id': symbol_id,
            'type': symbol_type,
            'name': name,
            'line': line,
            'scope': self.current_scope,
            'references': [],
            'definitions': [],
            'call_sites': [],
            'called_by': [],
            'properties': additional_info or {}
        }
        
        self.symbols[symbol_id] = symbol
        
        # Add to current scope
        if self.current_scope:
            current = self._get_scope(self.current_scope)
            if current:
                current['symbols'].append(symbol_id)
                
                # Add to appropriate category in scope
                if symbol_type == 'function':
                    current['functions'][name] = symbol_id
                elif symbol_type == 'variable':
                    current['variables'][name] = symbol_id
                elif symbol_type == 'class':
                    current['classes'][name] = symbol_id
                elif symbol_type in ['interface', 'type_alias']:
                    current['types'][name] = symbol_id
        
        return symbol_id
    
    def _process_functions(self, functions: List[Dict[str, Any]]):
        """Process function declarations."""
        for func in functions:
            func_name = func.get('name', '<anonymous>')
            line = func.get('line', 1)
            
            # Create function symbol
            symbol_id = self._create_symbol('function', func_name, line, {
                'kind': func.get('kind', 'function'),
                'parameters': func.get('parameters', []),
                'return_type': func.get('returnType'),
                'is_async': func.get('isAsync', False),
                'is_exported': func.get('isExported', False),
                'modifiers': func.get('modifiers', []),
                'type_parameters': func.get('typeParameters', []),
                'signature': func.get('signature', ''),
                'documentation': func.get('documentation')
            })
            
            # Create function scope for parameters and local variables
            if func_name != '<anonymous>':
                func_scope = self._create_scope('function', func_name, line, self.current_scope)
                self._enter_scope(func_scope)
                
                # Add parameters as symbols in function scope
                for param in func.get('parameters', []):
                    param_name = param.get('name', '')
                    if param_name:
                        self._create_symbol('parameter', param_name, line, {
                            'type': param.get('type', 'any'),
                            'optional': param.get('optional', False),
                            'default_value': param.get('defaultValue')
                        })
                
                self._exit_scope()
    
    def _process_classes(self, classes: List[Dict[str, Any]]):
        """Process class declarations."""
        for cls in classes:
            cls_name = cls.get('name', '<anonymous>')
            line = cls.get('line', 1)
            
            # Create class symbol
            symbol_id = self._create_symbol('class', cls_name, line, {
                'methods': cls.get('methods', []),
                'properties': cls.get('properties', []),
                'constructors': cls.get('constructors', []),
                'extends': cls.get('extends'),
                'implements': cls.get('implements', []),
                'is_abstract': cls.get('isAbstract', False),
                'is_exported': cls.get('isExported', False),
                'type_parameters': cls.get('typeParameters', []),
                'documentation': cls.get('documentation')
            })
            
            # Create class scope
            class_scope = self._create_scope('class', cls_name, line, self.current_scope)
            self._enter_scope(class_scope)
            
            # Process methods
            for method in cls.get('methods', []):
                method_name = method.get('name', '<anonymous>')
                method_line = method.get('line', line)
                
                self._create_symbol('method', method_name, method_line, {
                    'parameters': method.get('parameters', []),
                    'return_type': method.get('returnType'),
                    'is_static': method.get('isStatic', False),
                    'is_private': method.get('isPrivate', False),
                    'is_protected': method.get('isProtected', False),
                    'is_abstract': method.get('isAbstract', False),
                    'is_async': method.get('isAsync', False),
                    'parent_class': cls_name
                })
            
            # Process properties
            for prop in cls.get('properties', []):
                prop_name = prop.get('name', '<anonymous>')
                prop_line = prop.get('line', line)
                
                self._create_symbol('property', prop_name, prop_line, {
                    'type': prop.get('type'),
                    'is_static': prop.get('isStatic', False),
                    'is_private': prop.get('isPrivate', False),
                    'is_protected': prop.get('isProtected', False),
                    'is_readonly': prop.get('isReadonly', False),
                    'is_optional': prop.get('isOptional', False),
                    'initializer': prop.get('initializer'),
                    'parent_class': cls_name
                })
            
            # Process constructors
            for ctor in cls.get('constructors', []):
                self._create_symbol('constructor', 'constructor', ctor.get('line', line), {
                    'parameters': ctor.get('parameters', []),
                    'parent_class': cls_name
                })
            
            self._exit_scope()
    
    def _process_interfaces(self, interfaces: List[Dict[str, Any]]):
        """Process interface declarations."""
        for iface in interfaces:
            iface_name = iface.get('name', '<anonymous>')
            line = iface.get('line', 1)
            
            # Create interface symbol
            symbol_id = self._create_symbol('interface', iface_name, line, {
                'properties': iface.get('properties', []),
                'methods': iface.get('methods', []),
                'extends': iface.get('extends', []),
                'type_parameters': iface.get('typeParameters', []),
                'is_exported': iface.get('isExported', False),
                'documentation': iface.get('documentation')
            })
            
            # Create interface scope
            iface_scope = self._create_scope('interface', iface_name, line, self.current_scope)
            self._enter_scope(iface_scope)
            
            # Process properties
            for prop in iface.get('properties', []):
                prop_name = prop.get('name', '<anonymous>')
                prop_line = prop.get('line', line)
                
                self._create_symbol('interface_property', prop_name, prop_line, {
                    'type': prop.get('type'),
                    'is_optional': prop.get('isOptional', False),
                    'is_readonly': prop.get('isReadonly', False),
                    'parent_interface': iface_name
                })
            
            # Process methods
            for method in iface.get('methods', []):
                method_name = method.get('name', '<anonymous>')
                method_line = method.get('line', line)
                
                self._create_symbol('interface_method', method_name, method_line, {
                    'parameters': method.get('parameters', []),
                    'return_type': method.get('returnType'),
                    'is_optional': method.get('isOptional', False),
                    'parent_interface': iface_name
                })
            
            self._exit_scope()
    
    def _process_types(self, types: List[Dict[str, Any]]):
        """Process type alias declarations."""
        for type_alias in types:
            type_name = type_alias.get('name', '<anonymous>')
            line = type_alias.get('line', 1)
            
            # Create type symbol
            symbol_id = self._create_symbol('type_alias', type_name, line, {
                'type': type_alias.get('type'),
                'type_parameters': type_alias.get('typeParameters', []),
                'is_exported': type_alias.get('isExported', False),
                'documentation': type_alias.get('documentation')
            })
    
    def _process_variables(self, variables: List[Dict[str, Any]]):
        """Process variable declarations."""
        for var in variables:
            var_name = var.get('name', '<anonymous>')
            line = var.get('line', 1)
            
            # Create variable symbol
            symbol_id = self._create_symbol('variable', var_name, line, {
                'type': var.get('type'),
                'initializer': var.get('initializer'),
                'is_const': var.get('isConst', False),
                'is_let': var.get('isLet', False),
                'is_var': var.get('isVar', False)
            })
    
    def _process_imports(self, imports: List[Dict[str, Any]]):
        """Process import declarations."""
        for imp in imports:
            line = imp.get('line', 1)
            module_specifier = imp.get('moduleSpecifier', '')
            
            # Create import symbol
            symbol_id = self._create_symbol('import', f"import from {module_specifier}", line, {
                'module_specifier': module_specifier,
                'default_import': imp.get('defaultImport'),
                'namespace_import': imp.get('namespaceImport'),
                'named_imports': imp.get('namedImports', []),
                'is_type_only': imp.get('isTypeOnly', False)
            })
            
            # Create symbols for imported bindings
            if imp.get('defaultImport'):
                self._create_symbol('imported_binding', imp['defaultImport'], line, {
                    'import_type': 'default',
                    'source_module': module_specifier
                })
            
            if imp.get('namespaceImport'):
                self._create_symbol('imported_binding', imp['namespaceImport'], line, {
                    'import_type': 'namespace',
                    'source_module': module_specifier
                })
            
            for named_import in imp.get('namedImports', []):
                imported_name = named_import.get('name', '')
                if imported_name:
                    self._create_symbol('imported_binding', imported_name, line, {
                        'import_type': 'named',
                        'source_module': module_specifier,
                        'property_name': named_import.get('propertyName'),
                        'is_type_only': named_import.get('isTypeOnly', False)
                    })
    
    def _process_exports(self, exports: List[Dict[str, Any]]):
        """Process export declarations."""
        for exp in exports:
            line = exp.get('line', 1)
            
            # Create export symbol
            if exp.get('isDefault'):
                symbol_id = self._create_symbol('export', 'default export', line, {
                    'export_type': 'default',
                    'expression': exp.get('expression')
                })
            else:
                symbol_id = self._create_symbol('export', 'named export', line, {
                    'export_type': 'named',
                    'module_specifier': exp.get('moduleSpecifier'),
                    'named_exports': exp.get('namedExports', []),
                    'namespace_export': exp.get('namespaceExport')
                })
    
    def _build_call_relationships(self, call_expressions: List[Dict[str, Any]], 
                                 property_accesses: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Build call relationships between symbols."""
        relationships = {
            'function_calls': [],
            'method_calls': [],
            'property_accesses': [],
            'call_graph': {}
        }
        
        # Process function calls
        for call in call_expressions:
            expression = call.get('expression', '')
            line = call.get('line', 1)
            
            # Try to resolve the call target
            target_symbol = self._resolve_call_target(expression)
            
            call_info = {
                'expression': expression,
                'line': line,
                'arguments': call.get('arguments', []),
                'type_arguments': call.get('typeArguments', []),
                'target_symbol': target_symbol,
                'resolved': target_symbol is not None
            }
            
            if self._is_method_call(expression):
                relationships['method_calls'].append(call_info)
            else:
                relationships['function_calls'].append(call_info)
        
        # Process property accesses
        for access in property_accesses:
            relationships['property_accesses'].append({
                'expression': access.get('expression', ''),
                'property': access.get('name', ''),
                'line': access.get('line', 1),
                'full_expression': access.get('full', '')
            })
        
        # Build call graph
        relationships['call_graph'] = self._build_call_graph(relationships)
        
        return relationships
    
    def _resolve_call_target(self, expression: str) -> Optional[str]:
        """Try to resolve a call expression to a symbol ID."""
        # Simple resolution - look for exact name matches
        # In a full implementation, this would consider scope chains and imports
        
        # Extract the function/method name from the expression
        if '.' in expression:
            # Method call
            parts = expression.split('.')
            method_name = parts[-1]
        else:
            # Function call
            method_name = expression
        
        # Remove any parentheses or brackets
        method_name = re.sub(r'[(){}\[\]]', '', method_name).strip()
        
        # Look for matching symbols
        for symbol_id, symbol in self.symbols.items():
            if symbol['name'] == method_name and symbol['type'] in ['function', 'method']:
                return symbol_id
        
        return None
    
    def _is_method_call(self, expression: str) -> bool:
        """Check if an expression is a method call (contains dot notation)."""
        return '.' in expression
    
    def _build_call_graph(self, relationships: Dict[str, Any]) -> Dict[str, Any]:
        """Build a call graph from relationships."""
        graph = {}
        
        # Add nodes for all function/method symbols
        for symbol_id, symbol in self.symbols.items():
            if symbol['type'] in ['function', 'method', 'constructor']:
                graph[symbol_id] = {
                    'symbol': symbol,
                    'calls': [],
                    'called_by': []
                }
        
        # Add edges for calls
        all_calls = relationships['function_calls'] + relationships['method_calls']
        
        for call in all_calls:
            target_symbol = call.get('target_symbol')
            if target_symbol and target_symbol in graph:
                # Find the calling context (current scope/function)
                calling_symbol = self._get_calling_symbol(call['line'])
                if calling_symbol and calling_symbol in graph:
                    graph[calling_symbol]['calls'].append(target_symbol)
                    graph[target_symbol]['called_by'].append(calling_symbol)
        
        return graph
    
    def _get_calling_symbol(self, line: int) -> Optional[str]:
        """Get the symbol that contains the given line number."""
        # Find the innermost function/method that contains this line
        best_match = None
        best_line = 0
        
        for symbol_id, symbol in self.symbols.items():
            if symbol['type'] in ['function', 'method', 'constructor']:
                symbol_line = symbol['line']
                if symbol_line <= line and symbol_line > best_line:
                    best_match = symbol_id
                    best_line = symbol_line
        
        return best_match
    
    def get_symbol_by_name(self, name: str, symbol_type: Optional[str] = None) -> List[str]:
        """Get symbols by name and optionally type."""
        matching_symbols = []
        
        for symbol_id, symbol in self.symbols.items():
            if symbol['name'] == name:
                if symbol_type is None or symbol['type'] == symbol_type:
                    matching_symbols.append(symbol_id)
        
        return matching_symbols
    
    def get_symbols_in_scope(self, scope_id: str) -> List[str]:
        """Get all symbols in a specific scope."""
        scope = self._get_scope(scope_id)
        if scope:
            return scope['symbols'][:]
        return []
    
    def find_symbol_references(self, symbol_name: str) -> List[Dict[str, Any]]:
        """Find all references to a symbol by name."""
        references = []
        
        # Look in call expressions
        for symbol_id, symbol in self.symbols.items():
            if symbol['name'] == symbol_name:
                references.append({
                    'type': 'definition',
                    'symbol_id': symbol_id,
                    'line': symbol['line'],
                    'scope': symbol['scope']
                })
        
        return references